查看原文
其他

C++之智能指针

思想觉悟 思想觉悟 2022-10-09

导读

《C++之指针扫盲》一文中我们对指针进行了讲解,虽然原始指针是几乎无所不能,的确是一把利器,但就是这样的一把利器让多少人既爱又恨,一不小心就杀敌一千,自损八百,无论你是 多么的严谨,总是很难从根本上避免内存泄漏。

有没有好的方式去用好这把利刃而又不伤手呢?带着手套不就行了么。。。

RAII

在C程序中有一条行规是:

谁开发谁保护,谁污染谁治理

所以我们在很多库的API中经常发现一些传递二级指针的alloc函数和一些对应的xxx_free函数,这就是遵循谁开发谁保护,谁污染谁治理的原则。

在进入智能指针话题之前我们先来了解下RAII

RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。

堆指针在C/C++实在是太灵活,但是每次使用完毕后都需要程序员手动地去释放它,但是程序员们往往会忘记释放它,又或者是命名写了释放的代码,但是因为各种执行的异常情况,导致到释放资源的代码根本没有执行到,特别是引入了异常机制后的C++更是如此。因此为了解决这些问题,c++之父给出了解决问题的方案:

使用RAII,它充分地利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。

一句话总结起来就是在构造函数中去申请资源,在析构函数中去释放资源。

以下展示了一个简单的RAII的例子:

class Student{
public:

    Student():name(new string("张三")){

    }

    ~Student(){
        // 声明周期结束自动释放指针
        delete name;
        name = nullptr;
    }

public:
    const string *name;
};

int main() {
    Student student;
//    delete student.name; //  不需要手动释放内部的指针
    return 0;
}

咋一看,这RAII确实是个好东西呀,既然有了RAII还需要智能指针干嘛呢?难道C++造物主也要扛KPI???

智能指针

C++11推出了三种智能指针,它们分别是unique_ptr、shared_ptr和weak_ptr,同时也将auto_ptr废弃掉。既然auto_ptr已经废弃掉了,这里就不再讨论了, 感兴趣的童鞋可以自行查阅auto_ptr的相关隐患来了解下它为什么会被替代掉。

智能指针,既然是智能的,为什么需要三种呢?一种万能的不就可以了吗?存在即合理,unique_ptr、shared_ptr和weak_ptr三种智能指针在不同的场合区分使用更能提升我们程序的强壮性。

当需要使用智能指针的时候包含头文件<memory>即可。

1、unique_ptr

std::unique_ptr是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,也就是拷贝构造函数,赋值运算符这些相对于std::unique_ptr来说是不可用的了。如下例子可以验证这点:

int main() {
    unique_ptr<int> p1(new int(1));
    unique_ptr<int> p2 = p1; // 错误,unique_ptr不允许共享内部的原始指针
    unique_ptr<int> p3(new int(3));
    p3 = p1;               //错误,unique_ptr不允许共享内部的原始指针
    unique_ptr<int> p4 = std::move(p1);// 可以,但是此时p1是不可用的,p1中的指针已经转移到了p4
    std::cout << "p1:" << *p1 << std::endl; // 获取不到值
    return 0;
}

** 注意,在C++11 没有提供std::make_unique,它是在C++14之后才提供的。**

2、shared_ptr

显然std::unique_ptr智能指针的独占性在在一些场合是无法满足开发者们的需求的,此时就需要shared_ptr登场了,shared_ptr是基于引用计数的一种智能指针,shared_ptr内部维护着一个原始指针和一个引用计数的指针,当shared_ptr发生析构销毁的时候,shared_ptr会将引用计数减去1,如果引用计数不为0则不会销毁原始指针,直到引用计数 为0才会销毁这个原始指针。

相对于std::unique_ptr来说,shared_ptr的拷贝构造函数和赋值运算符是可以正常使用的,同时在C++11中就提供了make_shared函数,至于std::make_unique为什么到了C++14才提供, 俺也不知道,俺也不敢问呀....

shared_ptr可以通过use_count获取内部的原始指针的引用计数。

shared_ptr可以通过get函数获取到内部的原始指针,但是这是一件很疯狂的事情,意味着你需要对你自己做的事情负责....

int main() {
    shared_ptr<int> p1 = make_shared<int>(1);
    shared_ptr<int> p2 = p1; // 可以
    shared_ptr<int> p3(p1);
    std::cout << "p1 use_count:" << p1.use_count() << std::endl; // 3
    std::cout << "p2 use_count:" << p2.use_count() << std::endl; // 3
    std::cout << "p3 use_count:" << p3.use_count() << std::endl; // 3
    p1.reset(); // p1重置
    std::cout << "#######################" << std::endl;
    std::cout << "p1 use_count:" << p1.use_count() << std::endl; // 0
    std::cout << "p2 use_count:" << p2.use_count() << std::endl; // 2
    std::cout << "p3 use_count:" << p3.use_count() << std::endl; // 2
    std::cout << "*p1:" << *p1<< std::endl; // 无法获取到值
    return 0;
}

当然shared_ptr除了上面例子中用到的成员函数,还有其他的成员函数,大家需要多动手敲敲才能实践出真知。

与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:

int main() {
    unique_ptr<int[]> up(new int[10]); // 正确
    shared_ptr<int[]> sharedPtr1(new int[10]); // 错误
    shared_ptr<int> sharedPtr(new int[10], [](int *p) {
        std::cout << "释放指针" << std::endl;
        delete[] p;
    });  // 正确,要许自定义指针释放函数
    return 0;
}

3、weak_ptr

有了shared_ptrunique_ptr看来和内存泄漏说再见真是指日可待呀。

我们看看以下的例子,为什么他们的构造函数没有被调用?

main.cpp
using namespace std;
class A;
class B;
class A{
public:
    A(){

    }
    ~A(){
        std::cout << "A被析构" << std::endl;
    }
public:
    shared_ptr<B> ptrB;
};

class B{
public:
    B(){

    }
    ~B(){
        std::cout << "B被析构" << std::endl;
    }
public:
    shared_ptr<A> ptrA;
};

int main() {
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<B> pb = make_shared<B>();
    pa->ptrB = pb;
    pb->ptrA = pa;
    std::cout << "pa.use_count:" << pa.use_count() << std::endl;
    std::cout << "pb.use_count:" << pb.use_count() << std::endl;
    return 0;
}

很明显,已经发生了循环引用了,所以导致main函数执行完毕之后智能指针papb依然保持着1个引用计数,所以导致A和B都没有执行析构函数。

这是就需要weak_ptr来解决循环引用的问题了,weak_ptr是一种若引用的指针,它一般是搭配shared_ptr使用,但是它不会增加shared_ptr的引用计数,也就是说虽然我拥有你,但是我并限制你。。。

但也就是因为weak_ptr没有强引用,所以有可能在weak_ptr需要使用原始指针的时候,原始指针已经被别人释放掉了,所以在使用weak_ptr获取原始值之前需要使用lock校验一下。

weak_ptr一般不会单独使用,它一般都会使用一个shared_ptr来初始化它。

针对以上程序我们使用weak_ptr修复一下即可:

using namespace std;
class A;
class B;
class A{
public:
    A(){

    }
    ~A(){
        std::cout << "A被析构" << std::endl;
    }
public:
    weak_ptr<B> ptrB;
};

class B{
public:
    B(){

    }
    ~B(){
        std::cout << "B被析构" << std::endl;
    }
public:
    weak_ptr<A> ptrA;
};

int main() {
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<B> pb = make_shared<B>();
    pa->ptrB = pb;
    pb->ptrA = pa;
    std::cout << "pa.use_count:" << pa.use_count() << std::endl;
    std::cout << "pb.use_count:" << pb.use_count() << std::endl;
    shared_ptr<B> lock = pa->ptrB.lock(); // 如果已经被释放weak_ptr lock会返回空
    if(nullptr != lock){

    }
    return 0;
}

4、返回自己的智能指针

假如你希望通过智能指针来管理你的资源,在书写一个类的成员函数时你希望不返回原始的指向自己的指针,你希望是返回一个shared_ptr那么请看以下写法是否正确?

class A{
public:
    shared_ptr<A> getPtr(){
        return make_shared<A>();
    }
};

int main() {
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<A> ptr = pa->getPtr();
    std::cout << "pa.use_count" << pa.use_count() << std::endl; // 打印1
    std::cout << "ptr.use_count" << ptr.use_count() << std::endl; // 打印1
    return 0;
}

上面的写法会有什么问题呢?智能指针pa和智能指针ptr的引用计数都是1,但是他们却引用了同一个对象,这是很危险的,既然智能指针pa和智能指针ptr是相互独立的,并没有实现真正意义上的共享, 一旦他们当中的其中一个被析构,另外一个再去获取对象时就会发生意外。

如果想要在类的成员函数内返回自己的共享智能指针的话需要继承std::enable_shared_from_this<T>

class A:public std::enable_shared_from_this<A>{
public:
    shared_ptr<A> getPtr(){
        return shared_from_this();
    }
};

int main() {
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<A> ptr = pa->getPtr();
    std::cout << "pa.use_count" << pa.use_count() << std::endl; // 正常,打印2
    std::cout << "ptr.use_count" << ptr.use_count() << std::endl; // 正常,打印2
    return 0;
}

经过修改后我们发现智能指针pa和智能指针ptr实现了共享,他们的引用计数变成了2。

5、shared_ptr是线程安全的吗 这是一个面试官经常喜欢问的一道面试题。shared_ptr内部的引用计数是线程安全的,但是shared_ptr的指针不是线程安全的。大家可以想象下为什么shared_ptr引用计数要设计成线程安全的呢?

智能指针使用的建议

1、在使用指针指针时尽量使用标准库提供的初始化方法,不到万不得已,不要使用new的方式产生智能指针。这是因为 只有当一切构造动作都完成了,析构函数才有可能被调用。如果智能指针构造失败,那new方式传递进去的指针就不会被析构函数释放。

这条在《C++ Primer》一书中有说:

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

2、尽量不要获取智能指针内部的原始指针,在《C++ Primer》一书中有提到:

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

3、智能指针作为函数参数,能传引用则传递引用,否则使用值传递,虽然之间传递原始指针可能会损耗更加小的内存,但是这样会加大犯错的概率, 不太建议。

4、优先使用unique_ptr指针指针,如果确实需要共享的才使用shared_ptr,如果存在这循环引用的对象则shared_ptr搭配weak_ptr使用。

5、更多关于智能指针的资料,笔者建议童鞋们看看《Effective Modern C++》这本书的第四章内容。

关注我,一起进步,人生不止coding!!!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存